Проект: Market one click¶

Описание проекта

Интернет-магазин «В один клик» продаёт разные товары: для детей, для дома, мелкую бытовую технику, косметику и даже продукты. Отчёт магазина за прошлый период показал, что активность покупателей начала снижаться.
Привлекать новых клиентов уже не так эффективно: о магазине и так знает большая часть целевой аудитории.
Возможный выход — удерживать активность постоянных клиентов.
Сделать это можно с помощью персонализированных предложений.

Поставленная задача

Разработать модель, помогающую удерживать персональных клиентов с помощью персонализированных предложений.

Описание данных

market_file.csv - Таблица, которая содержит данные о поведении покупателя на сайте, о коммуникациях с покупателем и его продуктовом поведении.

  • id — номер покупателя в корпоративной базе данных.
  • Покупательская активность — рассчитанный класс покупательской активности (целевой признак): «снизилась» или «прежний уровень».
  • Тип сервиса — уровень сервиса, например «премиум» и «стандарт».
  • Разрешить сообщать — информация о том, можно ли присылать покупателю дополнительные предложения о товаре. Согласие на это даёт покупатель.
  • Маркет_актив_6_мес — среднемесячное значение маркетинговых коммуникаций компании, которое приходилось на покупателя за последние 6 месяцев. Это значение показывает, какое число рассылок, звонков, показов рекламы и прочего приходилось на клиента.
  • Маркет_актив_тек_мес — количество маркетинговых коммуникаций в текущем месяце.
  • Длительность — значение, которое показывает, сколько дней прошло с момента регистрации покупателя на сайте.
  • Акционные_покупки — среднемесячная доля покупок по акции от общего числа покупок за последние 6 месяцев.
  • Популярная_категория — самая популярная категория товаров у покупателя за последние 6 месяцев.
  • Средний_просмотр_категорий_за_визит — показывает, сколько в среднем категорий покупатель просмотрел за визит в течение последнего месяца.
  • Неоплаченные_продукты_штук_квартал — общее число неоплаченных товаров в корзине за последние 3 месяца.
  • Ошибка_сервиса — число сбоев, которые коснулись покупателя во время посещения сайта.
  • Страниц_за_визит — среднее количество страниц, которые просмотрел покупатель за один визит на сайт за последние 3 месяца.

market_money.csv - Таблица с данными о выручке, которую получает магазин с покупателя, то есть сколько покупатель всего потратил за период взаимодействия с сайтом.

  • id — номер покупателя в корпоративной базе данных.
  • Период — название периода, во время которого зафиксирована выручка. Например, 'текущий_месяц' или 'предыдущий_месяц'.
  • Выручка — сумма выручки за период.

market_time.csv - Таблица с данными о времени (в минутах), которое покупатель провёл на сайте в течение периода.

  • id — номер покупателя в корпоративной базе данных.
  • Период — название периода, во время которого зафиксировано общее время.
  • минут — значение времени, проведённого на сайте, в минутах.

money.csv - Таблица с данными о среднемесячной прибыли покупателя за последние 3 месяца: какую прибыль получает магазин от продаж каждому покупателю.

  • id — номер покупателя в корпоративной базе данных.
  • Прибыль — значение прибыли.

Table of Contents

  • 1  Загрузка данных
    • 1.1  Изучение загруженных датасетов
  • 2  Предобработка данных
    • 2.1  Приведение названий столбцов датафреймов к snake_case
    • 2.2  Проверка датафреймов на дубликаты
  • 3  Исследовательский анализ данных
    • 3.1  Функции для исследовательского анализа данных
    • 3.2  Анализ количественных и качественных признаков
    • 3.3  Выбор покупателей, активных в последние 3 месяца
  • 4  Объединение таблиц
  • 5  Корреляционный анализ
  • 6  Использование пайплайнов
  • 7  Анализ важности признаков
  • 8  Сегментация покупателей
  • 9  Общий вывод

Импортирование необходимых библиотек

In [1]:
%%capture
%pip install -U scikit-learn
In [2]:
!pip -q install phik
!pip -q install shap
# !pip -q install pandas
In [3]:
!pip -q install openpyxl
In [4]:
import pandas as pd
import matplotlib.pyplot as plt
import numpy as np
import re
import seaborn as sns
import shap
In [5]:
import warnings
warnings.simplefilter(action='ignore', category=FutureWarning)
In [6]:
from IPython.display import display
In [7]:
from phik import phik_matrix
from sklearn.pipeline import Pipeline
from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import (
    StandardScaler, OneHotEncoder,
    MinMaxScaler, LabelEncoder, OrdinalEncoder
)
from sklearn.impute import SimpleImputer
from sklearn.model_selection import train_test_split, RandomizedSearchCV, GridSearchCV
from sklearn.linear_model import LogisticRegression
from sklearn.neighbors import KNeighborsClassifier
from sklearn.tree import DecisionTreeClassifier
from sklearn.svm import SVC
from sklearn.metrics import roc_auc_score
from scipy.stats import shapiro
In [8]:
# Настройки вывода графиков
plt.rcParams["axes.titlesize"] = 16  # Размер шрифта
plt.rcParams["axes.titleweight"] = "bold"  # Толщина шрифта
In [9]:
# Константы
RANDOM_STATE = 42
TEST_SIZE = 0.25
TERM_SIZE = 180

Загрузка данных¶

In [10]:
try:
    market_file = pd.read_csv("C:\\Data-science\\ds_csv\\market_file.csv")
    market_money = pd.read_csv("C:\\Data-science\\ds_csv\\market_money.csv")
    market_time = pd.read_csv("C:\\Data-science\\ds_csv\\market_time.csv")
    money = pd.read_csv("C:\\Data-science\\ds_csv\\money.csv", sep=';', decimal=",")
except:
    try:
        market_file = pd.read_csv('/datasets/market_file.csv')
        market_money = pd.read_csv('/datasets/market_money.csv')
        market_time = pd.read_csv('/datasets/market_time.csv')
        money = pd.read_csv('/datasets/money.csv', sep=';', decimal=",")
    except:
        raise FileNotFoundError

Изучение загруженных датасетов¶

In [11]:
# Создаем словарь, чтобы перебрать все импортируемые датафреймы
dataframes = {
    "market_file": market_file,
    "market_money": market_money,
    "market_time": market_time,
    "money": money
}


# Цикл по каждому DataFrame с выводом имени, информации и первых строк
for name, data in dataframes.items():
    print(f'\033[1mНаименование анализируемого датафрейма:\033[0m {name}')
    print()
    data.info()  # Выводим информацию о DataFrame
    display(data.head(5))  # Отображаем первые 5 строк
    print('=' * TERM_SIZE)  # Линия-разделитель по ширине терминала
Наименование анализируемого датафрейма: market_file

<class 'pandas.core.frame.DataFrame'>
RangeIndex: 1300 entries, 0 to 1299
Data columns (total 13 columns):
 #   Column                               Non-Null Count  Dtype  
---  ------                               --------------  -----  
 0   id                                   1300 non-null   int64  
 1   Покупательская активность            1300 non-null   object 
 2   Тип сервиса                          1300 non-null   object 
 3   Разрешить сообщать                   1300 non-null   object 
 4   Маркет_актив_6_мес                   1300 non-null   float64
 5   Маркет_актив_тек_мес                 1300 non-null   int64  
 6   Длительность                         1300 non-null   int64  
 7   Акционные_покупки                    1300 non-null   float64
 8   Популярная_категория                 1300 non-null   object 
 9   Средний_просмотр_категорий_за_визит  1300 non-null   int64  
 10  Неоплаченные_продукты_штук_квартал   1300 non-null   int64  
 11  Ошибка_сервиса                       1300 non-null   int64  
 12  Страниц_за_визит                     1300 non-null   int64  
dtypes: float64(2), int64(7), object(4)
memory usage: 132.2+ KB
id Покупательская активность Тип сервиса Разрешить сообщать Маркет_актив_6_мес Маркет_актив_тек_мес Длительность Акционные_покупки Популярная_категория Средний_просмотр_категорий_за_визит Неоплаченные_продукты_штук_квартал Ошибка_сервиса Страниц_за_визит
0 215348 Снизилась премиум да 3.4 5 121 0.00 Товары для детей 6 2 1 5
1 215349 Снизилась премиум да 4.4 4 819 0.75 Товары для детей 4 4 2 5
2 215350 Снизилась стандартт нет 4.9 3 539 0.14 Домашний текстиль 5 2 1 5
3 215351 Снизилась стандартт да 3.2 5 896 0.99 Товары для детей 5 0 6 4
4 215352 Снизилась стандартт нет 5.1 3 1064 0.94 Товары для детей 3 2 3 2
====================================================================================================================================================================================
Наименование анализируемого датафрейма: market_money

<class 'pandas.core.frame.DataFrame'>
RangeIndex: 3900 entries, 0 to 3899
Data columns (total 3 columns):
 #   Column   Non-Null Count  Dtype  
---  ------   --------------  -----  
 0   id       3900 non-null   int64  
 1   Период   3900 non-null   object 
 2   Выручка  3900 non-null   float64
dtypes: float64(1), int64(1), object(1)
memory usage: 91.5+ KB
id Период Выручка
0 215348 препредыдущий_месяц 0.0
1 215348 текущий_месяц 3293.1
2 215348 предыдущий_месяц 0.0
3 215349 препредыдущий_месяц 4472.0
4 215349 текущий_месяц 4971.6
====================================================================================================================================================================================
Наименование анализируемого датафрейма: market_time

<class 'pandas.core.frame.DataFrame'>
RangeIndex: 2600 entries, 0 to 2599
Data columns (total 3 columns):
 #   Column  Non-Null Count  Dtype 
---  ------  --------------  ----- 
 0   id      2600 non-null   int64 
 1   Период  2600 non-null   object
 2   минут   2600 non-null   int64 
dtypes: int64(2), object(1)
memory usage: 61.1+ KB
id Период минут
0 215348 текущий_месяц 14
1 215348 предыдцщий_месяц 13
2 215349 текущий_месяц 10
3 215349 предыдцщий_месяц 12
4 215350 текущий_месяц 13
====================================================================================================================================================================================
Наименование анализируемого датафрейма: money

<class 'pandas.core.frame.DataFrame'>
RangeIndex: 1300 entries, 0 to 1299
Data columns (total 2 columns):
 #   Column   Non-Null Count  Dtype  
---  ------   --------------  -----  
 0   id       1300 non-null   int64  
 1   Прибыль  1300 non-null   float64
dtypes: float64(1), int64(1)
memory usage: 20.4 KB
id Прибыль
0 215348 0.98
1 215349 4.16
2 215350 3.13
3 215351 4.87
4 215352 4.21
====================================================================================================================================================================================
In [12]:
# установка индекса id датафрейма id покупателя
dfs = [
    market_file,
    market_money,
    market_time,
    money
]


for df in dfs:
    df = df.set_index('id')
    # Проверка
    display(df.head(1))
Покупательская активность Тип сервиса Разрешить сообщать Маркет_актив_6_мес Маркет_актив_тек_мес Длительность Акционные_покупки Популярная_категория Средний_просмотр_категорий_за_визит Неоплаченные_продукты_штук_квартал Ошибка_сервиса Страниц_за_визит
id
215348 Снизилась премиум да 3.4 5 121 0.0 Товары для детей 6 2 1 5
Период Выручка
id
215348 препредыдущий_месяц 0.0
Период минут
id
215348 текущий_месяц 14
Прибыль
id
215348 0.98

Вывод по предварительному анализу:

В ходе предварительного анализа данных было выявлено:

  • В датафреймах отсвутствуют пропуски;
  • Название столбцов датафреймов необходимо привести к snake_case;
  • Нет ошибок в типах данных в датафреймах;
  • Столбцы id в датафреймах сделали значениями индексов;
  • Была получена общая информация о датафреймах.

Предобработка данных¶

Приведение названий столбцов датафреймов к snake_case¶

In [13]:
# приведение названий столбцов датафреймов в snake_case
for df in dfs:
    df.columns = [re.sub(r'(?<!^)(?=[A-Z])', '_', i). replace(' ', '_').lower() for i in df.columns]

# проверка
for df in dfs:
    df.info()
    print('=' * TERM_SIZE)
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 1300 entries, 0 to 1299
Data columns (total 13 columns):
 #   Column                               Non-Null Count  Dtype  
---  ------                               --------------  -----  
 0   id                                   1300 non-null   int64  
 1   покупательская_активность            1300 non-null   object 
 2   тип_сервиса                          1300 non-null   object 
 3   разрешить_сообщать                   1300 non-null   object 
 4   маркет_актив_6_мес                   1300 non-null   float64
 5   маркет_актив_тек_мес                 1300 non-null   int64  
 6   длительность                         1300 non-null   int64  
 7   акционные_покупки                    1300 non-null   float64
 8   популярная_категория                 1300 non-null   object 
 9   средний_просмотр_категорий_за_визит  1300 non-null   int64  
 10  неоплаченные_продукты_штук_квартал   1300 non-null   int64  
 11  ошибка_сервиса                       1300 non-null   int64  
 12  страниц_за_визит                     1300 non-null   int64  
dtypes: float64(2), int64(7), object(4)
memory usage: 132.2+ KB
====================================================================================================================================================================================
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 3900 entries, 0 to 3899
Data columns (total 3 columns):
 #   Column   Non-Null Count  Dtype  
---  ------   --------------  -----  
 0   id       3900 non-null   int64  
 1   период   3900 non-null   object 
 2   выручка  3900 non-null   float64
dtypes: float64(1), int64(1), object(1)
memory usage: 91.5+ KB
====================================================================================================================================================================================
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 2600 entries, 0 to 2599
Data columns (total 3 columns):
 #   Column  Non-Null Count  Dtype 
---  ------  --------------  ----- 
 0   id      2600 non-null   int64 
 1   период  2600 non-null   object
 2   минут   2600 non-null   int64 
dtypes: int64(2), object(1)
memory usage: 61.1+ KB
====================================================================================================================================================================================
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 1300 entries, 0 to 1299
Data columns (total 2 columns):
 #   Column   Non-Null Count  Dtype  
---  ------   --------------  -----  
 0   id       1300 non-null   int64  
 1   прибыль  1300 non-null   float64
dtypes: float64(1), int64(1)
memory usage: 20.4 KB
====================================================================================================================================================================================

Проверка датафреймов на дубликаты¶

Проверка на явные дубликаты

In [14]:
# Обновление словаря
dataframes = {
    "market_file": market_file,
    "market_money": market_money,
    "market_time": market_time,
    "money": money
}

for name, data in dataframes.items():
    print(f"Явных дубликатов в датасете {name} - {data.duplicated().sum()}: "
          if data.duplicated().sum() > 0 else f"Явных дубликатов в датасете {name} - НЕТ")
Явных дубликатов в датасете market_file - НЕТ
Явных дубликатов в датасете market_money - НЕТ
Явных дубликатов в датасете market_time - НЕТ
Явных дубликатов в датасете money - НЕТ

Проверка на неявные дубликаты

In [15]:
for name, data in dataframes.items():
    col_cat = data.select_dtypes(include=['object']).columns.to_list()
    print(f'Название датафрейма {name}')
    for col in col_cat:
        print(f"Уникальные значения в столбце '{col}': {data[col].unique()}")
    print('=' * TERM_SIZE)
Название датафрейма market_file
Уникальные значения в столбце 'покупательская_активность': ['Снизилась' 'Прежний уровень']
Уникальные значения в столбце 'тип_сервиса': ['премиум' 'стандартт' 'стандарт']
Уникальные значения в столбце 'разрешить_сообщать': ['да' 'нет']
Уникальные значения в столбце 'популярная_категория': ['Товары для детей' 'Домашний текстиль' 'Косметика и аксесуары'
 'Техника для красоты и здоровья' 'Кухонная посуда'
 'Мелкая бытовая техника и электроника']
====================================================================================================================================================================================
Название датафрейма market_money
Уникальные значения в столбце 'период': ['препредыдущий_месяц' 'текущий_месяц' 'предыдущий_месяц']
====================================================================================================================================================================================
Название датафрейма market_time
Уникальные значения в столбце 'период': ['текущий_месяц' 'предыдцщий_месяц']
====================================================================================================================================================================================
Название датафрейма money
====================================================================================================================================================================================
In [16]:
# Переименование дубликатов
market_file.loc[market_file['тип_сервиса'] == 'стандартт', 'тип_сервиса'] = 'стандарт'
market_time.loc[market_time['период'] == 'предыдцщий_месяц', 'период'] = 'предыдущий_месяц'

# Проверка
display(market_file['тип_сервиса'].unique())
market_time['период'].unique()
array(['премиум', 'стандарт'], dtype=object)
Out[16]:
array(['текущий_месяц', 'предыдущий_месяц'], dtype=object)
In [17]:
# Обновление словаря
dataframes = {
    "market_file": market_file,
    "market_money": market_money,
    "market_time": market_time,
    "money": money
}

for name, data in dataframes.items():
    print(f"Явных дубликатов в датасете {name} - {data.duplicated().sum()}: "
          if data.duplicated().sum() > 0 else f"Явных дубликатов в датасете {name} - НЕТ")
Явных дубликатов в датасете market_file - НЕТ
Явных дубликатов в датасете market_money - НЕТ
Явных дубликатов в датасете market_time - НЕТ
Явных дубликатов в датасете money - НЕТ

Вывод по предобработке данных:

В ходе предобработке данных было выполнено:

  • Приведены названия столбцов датафреймов к snake_case;
  • Проверены датафреймы на дубликаты и выявлено их отсутствие.

Исследовательский анализ данных¶

Функции для исследовательского анализа данных¶

In [18]:
def show_num_variable(df, column, target=None):
    '''
    Функция отображения гистограммы распределения
    и диаграммы размаха для определенного столбца датафрейма
    с учетом принадлежности данного столбца к разным значениям
    переменной target.
    
    Параметры:
    - df: pandas.DataFrame, входной датафрейм
    - column: str, столбец для анализа
    - target: str или None, столбец для группировки (по умолчанию None)
    '''
    sns.set()
    f, axes = plt.subplots(1, 2, figsize=(16, 6))
    
    # Гистограмма
    axes[0].set_title(f'Гистограмма для {column}', fontsize=16)
    axes[0].set_ylabel('Количество', fontsize=14)
    if target:
        sns.histplot(data=df, bins=20, kde=True, ax=axes[0], hue=target, x=column)
    else:
        sns.histplot(data=df, bins=20, kde=True, ax=axes[0], x=column)
    
    # Диаграмма размаха
    axes[1].set_title(f'Диаграмма размаха для {column}', fontsize=16)
    if target:
        sns.boxplot(data=df, ax=axes[1], x=target, y=column)
    else:
        sns.boxplot(data=df, ax=axes[1], y=column, orient='v')
    axes[1].set_ylabel(column, fontsize=14)
    
    plt.tight_layout()
    plt.show()
In [19]:
def show_cat_variable_by_target(df, column, title, target=None, rot=60):
    '''
    Функция отображения соотношения категориальных признаков
    в столбце датафрейма, разделенных по значениям целевого признака.
    Если target не передан, отображается только countplot для column.
    Добавляет проценты на график.
    
    :param df: DataFrame, датафрейм с данными
    :param column: str, название столбца, по которому строится график
    :param title: str, заголовок графика
    :param target: str, название столбца с целевым признаком (по умолчанию None)
    :param rot: int, угол поворота меток на оси X
    '''
    
    # Если target не указан, рисуем только countplot для одного признака
    if target is None:
        plt.figure(figsize=(12, 6))
        ax = sns.countplot(data=df, x=column)
        ax.set_title(f"{title}", fontsize=14)
        ax.set_xlabel(column, fontsize=12)
        ax.set_ylabel('Количество', fontsize=12)
        ax.tick_params(axis='x', rotation=rot)
        
        # Добавляем проценты на график
        total_count = len(df)
        for p in ax.patches:
            count = p.get_height()
            if count > 0:  # Добавляем проценты только если высота столбца > 0
                percentage = f'{100 * count / total_count:.1f}%'
                ax.annotate(percentage,
                            (p.get_x() + p.get_width() / 2., count),
                            ha='center', va='center', 
                            xytext=(0, 10), 
                            textcoords='offset points',
                            fontsize=12, color='black')
        
        plt.tight_layout()
        plt.show()
        return
    
    # Проверяем, что столбец target существует в DataFrame
    if target not in df.columns:
        raise ValueError(f"Столбец '{target}' не найден в DataFrame.")
    
    # Если target указан, строим графики для каждого уникального значения target
    unique_targets = df[target].unique()
    num_targets = len(unique_targets)
    
    # Определяем размер холста в зависимости от количества таргетов
    fig, axes = plt.subplots(nrows=num_targets, ncols=1, figsize=(12, 5 * num_targets), sharex=True)
    
    # Если уникальных значений больше одного, axes будет массивом
    # Если только одно значение, делаем axes списком для унификации
    if num_targets == 1:
        axes = [axes]
    
    # Создаем график для каждого уникального значения целевого признака
    for i, target_value in enumerate(unique_targets):
        # Создаем подмножество данных для текущего значения целевого признака
        subset = df[df[target] == target_value]
        
        # Получаем уникальные значения категориального признака для сортировки
        categories = subset[column].value_counts().index.tolist()
        
        # Создаем countplot для текущего значения целевого признака
        ax = sns.countplot(data=subset, x=column, ax=axes[i], order=categories)
        
        # Заголовок и настройка осей
        axes[i].set_title(f"{title}: {target} - {target_value}", fontsize=14)
        axes[i].set_xlabel(column if i == num_targets - 1 else "", fontsize=12)  # Подпись только для нижнего графика
        axes[i].set_ylabel('Количество', fontsize=12)
        axes[i].tick_params(axis='x', rotation=rot)
        
        # Вычисляем общее количество для текущей группы
        total_count = len(subset)
        
        # Добавляем проценты на столбцы
        for p in ax.patches:
            count = p.get_height()
            if count > 0:  # Добавляем проценты только если высота столбца > 0
                percentage = f'{100 * count / total_count:.1f}%'
                # Вычисляем координаты для аннотации (верхняя часть столбца)
                ax.annotate(percentage,
                            (p.get_x() + p.get_width() / 2., count),
                            ha='center', va='center', 
                            xytext=(0, 10), 
                            textcoords='offset points',
                            fontsize=12, color='black')
    
    # Улучшаем отображение
    plt.tight_layout(h_pad=2.0)  # Добавляем вертикальный отступ между графиками
    plt.show()
In [20]:
def normal_check(data, column, alpha=0.05):
    '''
    Функция проверки нормальности распределения
    по тесту Шапиро — Уилка
    '''
    stat, p = shapiro(data[column])
    print(f"Тест Шапиро — Уилка: Stat={stat}, p={p}")

    # Результат
    if p > alpha:
        return print(f"Распределение данных нормальное с вероятностью более {1 - alpha}.")
    else:
        return print(f"Распределение данных не нормальное с вероятностью более {1 - alpha}.")

Анализ количественных и качественных признаков¶

Датафрейм market_file

In [21]:
# Формирование списка столбцов с количественными признаками
num_variables_col = market_file.select_dtypes(include=['number']).columns.to_list()
num_variables_col.remove('id')
num_variables_col
Out[21]:
['маркет_актив_6_мес',
 'маркет_актив_тек_мес',
 'длительность',
 'акционные_покупки',
 'средний_просмотр_категорий_за_визит',
 'неоплаченные_продукты_штук_квартал',
 'ошибка_сервиса',
 'страниц_за_визит']
In [22]:
# Вывод графиков для датафрейма market_file
for col in num_variables_col:
    show_num_variable(market_file, col, 'покупательская_активность')
    print('=' * TERM_SIZE)
    
No description has been provided for this image
====================================================================================================================================================================================
No description has been provided for this image
====================================================================================================================================================================================
No description has been provided for this image
====================================================================================================================================================================================
No description has been provided for this image
====================================================================================================================================================================================
No description has been provided for this image
====================================================================================================================================================================================
No description has been provided for this image
====================================================================================================================================================================================
No description has been provided for this image
====================================================================================================================================================================================
No description has been provided for this image
====================================================================================================================================================================================
In [23]:
stats_df = market_file.groupby('покупательская_активность')[num_variables_col].describe().round(3).T
try:
    stats_df.to_excel('output.xlsx')
except:
    display(stats_df)
In [24]:
# Проверка на нормальность распределения с помощью теста Шапиро
for group in market_file['покупательская_активность'].unique():
    print(f"\033[1mПроверка нормальности для группы {group}\033[0m:")
    for col in num_variables_col:
        print(f'\033[1mДля столбца:\033[0m {col}')
        normal_check(market_file[market_file['покупательская_активность'] == group], col)
        print('=' * TERM_SIZE)
        
Проверка нормальности для группы Снизилась:
Для столбца: маркет_актив_6_мес
Тест Шапиро — Уилка: Stat=0.9748254418373108, p=1.480918996321634e-07
Распределение данных не нормальное с вероятностью более 0.95.
====================================================================================================================================================================================
Для столбца: маркет_актив_тек_мес
Тест Шапиро — Уилка: Stat=0.8077908754348755, p=6.791739773128278e-24
Распределение данных не нормальное с вероятностью более 0.95.
====================================================================================================================================================================================
Для столбца: длительность
Тест Шапиро — Уилка: Stat=0.9783022403717041, p=9.273203431803267e-07
Распределение данных не нормальное с вероятностью более 0.95.
====================================================================================================================================================================================
Для столбца: акционные_покупки
Тест Шапиро — Уилка: Stat=0.7598561644554138, p=2.6357747202963015e-26
Распределение данных не нормальное с вероятностью более 0.95.
====================================================================================================================================================================================
Для столбца: средний_просмотр_категорий_за_визит
Тест Шапиро — Уилка: Stat=0.8931688070297241, p=3.993350478310506e-18
Распределение данных не нормальное с вероятностью более 0.95.
====================================================================================================================================================================================
Для столбца: неоплаченные_продукты_штук_квартал
Тест Шапиро — Уилка: Stat=0.9590765237808228, p=1.5509174500216716e-10
Распределение данных не нормальное с вероятностью более 0.95.
====================================================================================================================================================================================
Для столбца: ошибка_сервиса
Тест Шапиро — Уилка: Stat=0.9374690651893616, p=1.3106536526658746e-13
Распределение данных не нормальное с вероятностью более 0.95.
====================================================================================================================================================================================
Для столбца: страниц_за_визит
Тест Шапиро — Уилка: Stat=0.8759505152702332, p=1.639263240222402e-19
Распределение данных не нормальное с вероятностью более 0.95.
====================================================================================================================================================================================
Проверка нормальности для группы Прежний уровень:
Для столбца: маркет_актив_6_мес
Тест Шапиро — Уилка: Stat=0.9850596189498901, p=2.698477317153447e-07
Распределение данных не нормальное с вероятностью более 0.95.
====================================================================================================================================================================================
Для столбца: маркет_актив_тек_мес
Тест Шапиро — Уилка: Stat=0.8043734431266785, p=5.030570392140609e-30
Распределение данных не нормальное с вероятностью более 0.95.
====================================================================================================================================================================================
Для столбца: длительность
Тест Шапиро — Уилка: Stat=0.9670731425285339, p=1.8170366470129928e-12
Распределение данных не нормальное с вероятностью более 0.95.
====================================================================================================================================================================================
Для столбца: акционные_покупки
Тест Шапиро — Уилка: Stat=0.5570206642150879, p=7.862685683326549e-41
Распределение данных не нормальное с вероятностью более 0.95.
====================================================================================================================================================================================
Для столбца: средний_просмотр_категорий_за_визит
Тест Шапиро — Уилка: Stat=0.9318152070045471, p=1.1972408492866681e-18
Распределение данных не нормальное с вероятностью более 0.95.
====================================================================================================================================================================================
Для столбца: неоплаченные_продукты_штук_квартал
Тест Шапиро — Уилка: Stat=0.9326432943344116, p=1.5603153389521451e-18
Распределение данных не нормальное с вероятностью более 0.95.
====================================================================================================================================================================================
Для столбца: ошибка_сервиса
Тест Шапиро — Уилка: Stat=0.9724842309951782, p=3.7529094415456044e-11
Распределение данных не нормальное с вероятностью более 0.95.
====================================================================================================================================================================================
Для столбца: страниц_за_визит
Тест Шапиро — Уилка: Stat=0.980556070804596, p=7.650830191607838e-09
Распределение данных не нормальное с вероятностью более 0.95.
====================================================================================================================================================================================

Вывод по полученным графикам количественных признаков в датафрейме market_file:

  • Распределения всех количественных признаков в датафрейме market_file отличаются от Гауссовского;
  • Поведение клиентов с разным целевым признаком - покупательской_активность отличаются:
    • Покупательская активность в среднем выше для тех клиентов, у которых больший показатель маркет_актив_6_мес (покупательская_активность - Прежний уровень: mean = 4.57 и median = 4.4, покупательская_активность - Снизилась: mean = 4.57 и median = 4.4), что может указывать на большую вовлеченность тех клиентов, которым чаще приходит рассылка;
    • Клиенты, чья активность снизилась, в среднем проводят больше времени, на что указывает среднее и медианное значение показателя длительность (покупательская_активность - Прежний уровень: mean = 590 и median = 590, покупательская_активность - Снизилась: mean = 620 и median = 634);
    • Признак акционные_покупки распределен бимодально, с явным разделением на две категории (1-я <= 0.65, 2-я > 0.65). Поэтому для подготовки данных к модели целесообразно разделить пользователей на две группы: часто покупает по акции и редко покупает по акции, преобразовав колонку Акционные_покупки в категориальный признак;
    • покупательской_активность также положительно влияет на количество просмотренных страниц и категорий, средний_просмотр_категорий_за_визит (покупательская_активность - Прежний уровень: mean = 3.67 и median = 4, покупательская_активность - Снизилась: mean = 2.63 и median = 2), страниц_за_визит (покупательская_активность - Прежний уровень: mean = 9.8 и median = 10, покупательская_активность - Снизилась: mean = 5.57 и median = 5);
    • Значение признака ошибка_сервиса практически не зависит от покупательской_активность (покупательская_активность - Прежний уровень: mean = 4.3 и median = 4, покупательская_активность - Снизилась: mean = 3.94 и median = 4);
    • Значение признака неоплаченные_продукты_штук_квартал ниже у активных клиентов (покупательская_активность - Прежний уровень: mean = 2.23 и median = 2, покупательская_активность - Снизилась: mean = 3.72 и median = 4).
In [25]:
market_file.groupby('покупательская_активность').describe(include='object').round(3).T
Out[25]:
покупательская_активность Прежний уровень Снизилась
тип_сервиса count 802 498
unique 2 2
top стандарт стандарт
freq 596 328
разрешить_сообщать count 802 498
unique 2 2
top да да
freq 591 371
популярная_категория count 802 498
unique 6 6
top Товары для детей Товары для детей
freq 184 146
In [26]:
show_cat_variable_by_target(market_file, 'покупательская_активность', 'покупательская_активность')
No description has been provided for this image
In [27]:
# Лист с названиями столбцов категориальных признаков
dict_market_file_cat = market_file.select_dtypes(include=['object']).columns.to_list()

dict_market_file_cat.remove('покупательская_активность')

for col in dict_market_file_cat:
    show_cat_variable_by_target(market_file, col, col, 'покупательская_активность')
    print('=' * TERM_SIZE)
No description has been provided for this image
====================================================================================================================================================================================
No description has been provided for this image
====================================================================================================================================================================================
No description has been provided for this image
====================================================================================================================================================================================

Вывод по полученным графикам категориальных признаков датафрейма market_file:

Целевой признак покупательская_активность не привносит существенный вклад на категориальные признаки;
Целевой признак покупательская_активность разделен на две категории "Снизилась" 38.3 % наблюдений и "Прежний уровень" - 61.7 % наблюдения;
Наблюдается дисбаланс в биномиальных категориальных признаках: тип_сервиса и разрешить_сообщать;
Признак популярная_категория мультиклассовый, наиболее популярный класс - Товары для детей (25.4 %), а наименее популярный Кухонная посуда (10.6 %).

Датафрейм market_money

In [28]:
show_num_variable(market_money, 'выручка', 'период')
No description has been provided for this image
In [29]:
emissions_id = market_money.query('выручка > 8000')['id']
emissions_id
Out[29]:
98    215380
Name: id, dtype: int64
In [30]:
market_file.query('id in @emissions_id')
Out[30]:
id покупательская_активность тип_сервиса разрешить_сообщать маркет_актив_6_мес маркет_актив_тек_мес длительность акционные_покупки популярная_категория средний_просмотр_категорий_за_визит неоплаченные_продукты_штук_квартал ошибка_сервиса страниц_за_визит
32 215380 Снизилась премиум нет 1.7 4 637 0.94 Техника для красоты и здоровья 3 2 4 7

Замечен единичный выброс для суммы выручки за текущий месяц, уберем его

In [31]:
market_money = market_money.query('выручка < 8000')
In [32]:
market_money.groupby('период')['выручка'].describe().round(3).T
Out[32]:
период предыдущий_месяц препредыдущий_месяц текущий_месяц
count 1300.000 1300.000 1299.000
mean 4936.920 4825.207 5236.787
std 739.598 405.980 835.475
min 0.000 0.000 2758.700
25% 4496.750 4583.000 4705.500
50% 5005.000 4809.000 5179.600
75% 5405.625 5053.500 5759.950
max 6869.500 5663.000 7799.400
In [33]:
show_num_variable(market_money, 'выручка', 'период')
No description has been provided for this image
In [34]:
for group in market_money['период'].unique():
    print(f"\033[1mПроверка нормальности для группы {group}\033[0m:")
    print(f'\033[1mДля столбца:\033[0m {"выручка"}')
    normal_check(market_money[market_money['период'] == group], 'выручка')
    print('=' * TERM_SIZE)
Проверка нормальности для группы препредыдущий_месяц:
Для столбца: выручка
Тест Шапиро — Уилка: Stat=0.7978613376617432, p=1.993647055477295e-37
Распределение данных не нормальное с вероятностью более 0.95.
====================================================================================================================================================================================
Проверка нормальности для группы текущий_месяц:
Для столбца: выручка
Тест Шапиро — Уилка: Stat=0.9947782158851624, p=0.00017225794726982713
Распределение данных не нормальное с вероятностью более 0.95.
====================================================================================================================================================================================
Проверка нормальности для группы предыдущий_месяц:
Для столбца: выручка
Тест Шапиро — Уилка: Stat=0.9597474336624146, p=1.7770246653221564e-18
Распределение данных не нормальное с вероятностью более 0.95.
====================================================================================================================================================================================
In [35]:
show_cat_variable_by_target(market_money, "период", "период", rot=0)
No description has been provided for this image

Вывод по анализу графиков для датафрейма market_money

  • Категориальный признак период распределен равномерно, дисбаланс отсутствует;
  • Выручка схожа для разных периодов, минимальная десперсия выручки у периода препредыдущий_месяц.

market_time

In [36]:
market_time.groupby('период')['минут'].describe().round(3).T
Out[36]:
период предыдущий_месяц текущий_месяц
count 1300.000 1300.000
mean 13.468 13.205
std 3.932 4.221
min 5.000 4.000
25% 11.000 10.000
50% 13.000 13.000
75% 17.000 16.000
max 23.000 23.000
In [37]:
show_num_variable(market_time, 'минут', 'период')
No description has been provided for this image
In [38]:
for group in market_time['период'].unique():
    print(f"\033[1mПроверка нормальности для группы {group}\033[0m:")
    print(f'\033[1mДля столбца:\033[0m {"минут"}')
    normal_check(market_time[market_time['период'] == group], 'минут')
    print('=' * TERM_SIZE)
Проверка нормальности для группы текущий_месяц:
Для столбца: минут
Тест Шапиро — Уилка: Stat=0.9797751903533936, p=1.5868249639630627e-12
Распределение данных не нормальное с вероятностью более 0.95.
====================================================================================================================================================================================
Проверка нормальности для группы предыдущий_месяц:
Для столбца: минут
Тест Шапиро — Уилка: Stat=0.9828875660896301, p=2.845738401868747e-11
Распределение данных не нормальное с вероятностью более 0.95.
====================================================================================================================================================================================
In [39]:
show_cat_variable_by_target(market_time, "период", "период", rot=0)
No description has been provided for this image

Вывод по анализу графиков для датафрейма market_time

  • Дисбаланса категориальных признаков в датафрейме не замечено;
  • Отличие проведенного времяни на сайте в разные периоды времени не замечено.

money

In [40]:
money.describe().round(3)
Out[40]:
id прибыль
count 1300.000 1300.000
mean 215997.500 3.997
std 375.422 1.014
min 215348.000 0.860
25% 215672.750 3.300
50% 215997.500 4.045
75% 216322.250 4.670
max 216647.000 7.430
In [41]:
show_num_variable(money, 'прибыль')
No description has been provided for this image
In [42]:
normal_check(money, 'прибыль')
Тест Шапиро — Уилка: Stat=0.9983819723129272, p=0.25812551379203796
Распределение данных нормальное с вероятностью более 0.95.

Признак прибыль распределен нормально. mean = 3.997 и median = 4.045

Выбор покупателей, активных в последние 3 месяца¶

Покупателя можно считать активным, если выручка за его деятельность во всех 3-х месяцах больше 0. Т.е. нам нужны записи из market_money, где у клиента есть покупки за все три периода 'препредыдущий_месяц', 'текущий_месяц', 'предыдущий_месяц'.

In [43]:
# преобразование данных
market_money = market_money.pivot(index='id', columns='период', values='выручка')
market_money.columns = ['выручка_' + str(col) for col in market_money.columns]


# упорядочивание столбцов
market_money = market_money[['выручка_препредыдущий_месяц', 'выручка_предыдущий_месяц', 'выручка_текущий_месяц']]


# фильтрация данных
market_money = market_money[(market_money['выручка_препредыдущий_месяц'] > 0) & \
                            (market_money['выручка_предыдущий_месяц'] > 0) & (market_money['выручка_текущий_месяц'] > 0)]


display(market_money.sample(5))
market_money.shape
выручка_препредыдущий_месяц выручка_предыдущий_месяц выручка_текущий_месяц
id
215400 4439.0 5681.0 5691.4
215784 4903.0 4165.0 3655.4
216217 4703.0 4091.0 3690.6
216125 5235.0 5358.0 5594.6
215881 4786.0 5084.0 4955.3
Out[43]:
(1296, 3)

Общий вывод по исследовательскому анализу данных:

В ходе исследовательского анализа было замечено влияние целевого признака покупательская_актиивность на остальные.

Анализ данных показал, что большинство пользователей выбирает стандартный тип сервиса, разрешать_сообщать = да и предпочитает раздел «Товары для детей».

Пользователи, чья активность снизилась, в среднем проводят больше времени на сайте и совершают больше покупок со скидками. Возможно, это связано с тем, что они ищут более выгодные предложения или скидки, что указывает на их чувствительность к ценам. Если это действительно так, то предоставление специальных акций или скидок этой группе пользователей может повысить их активность и прибыль.


Объединение таблиц¶

In [44]:
# преобразование данных
market_time = market_time.pivot(index='id', columns='период', values='минут')
market_time.columns = ['минут_' + str(col) for col in market_time.columns]

display(market_time)
минут_предыдущий_месяц минут_текущий_месяц
id
215348 13 14
215349 12 10
215350 8 13
215351 11 13
215352 8 11
... ... ...
216643 14 7
216644 12 11
216645 12 18
216646 18 7
216647 15 10

1300 rows × 2 columns

In [45]:
# объединение данных
market_full = market_file.merge(market_money, on='id', how='inner')
market_full = market_full.merge(market_time, on='id', how='inner')

# Проверка
display(market_full.sample(5))
market_full.shape
id покупательская_активность тип_сервиса разрешить_сообщать маркет_актив_6_мес маркет_актив_тек_мес длительность акционные_покупки популярная_категория средний_просмотр_категорий_за_визит неоплаченные_продукты_штук_квартал ошибка_сервиса страниц_за_визит выручка_препредыдущий_месяц выручка_предыдущий_месяц выручка_текущий_месяц минут_предыдущий_месяц минут_текущий_месяц
93 215445 Снизилась стандарт нет 4.0 5 372 0.99 Косметика и аксесуары 1 6 6 5 4528.0 5186.5 5208.6 9 10
287 215639 Снизилась премиум да 2.7 5 845 0.32 Товары для детей 2 2 5 4 4379.0 5730.5 6957.7 6 10
593 215945 Прежний уровень премиум да 4.0 3 1007 0.23 Кухонная посуда 2 3 6 7 5004.0 4420.5 4044.8 19 23
79 215431 Снизилась премиум да 3.9 4 666 0.21 Кухонная посуда 3 3 6 7 4692.0 5455.5 6184.8 12 14
1263 216615 Прежний уровень стандарт да 5.1 4 780 0.27 Домашний текстиль 4 1 4 4 5360.0 5051.0 4913.2 11 12
Out[45]:
(1296, 18)

В ходе объединения таблиц было созданы отдельны столбецы для каждого периода по выручке и времени.


Корреляционный анализ¶

Для корреляционного анализа и впоследствии модели машинного обучения, признак id не нужен, т.к. не несет никакой смысловой нагрузки.

In [46]:
market_full_wthout_id = market_full.drop('id', axis=1)
In [47]:
col_names_corr = market_full_wthout_id.select_dtypes(include='number').columns.to_list()

big_data_corr = market_full_wthout_id.phik_matrix(interval_cols=col_names_corr)

plt.figure(figsize=(14, 10))
sns.heatmap(big_data_corr, annot=True, fmt=".2f", center=0)
plt.title('Матрица корреляций phik')
plt.show()
No description has been provided for this image

Вывод по корреляционному анализу

Мультиколлеанарности между признаками не замечено;

В ходе анализа Phik матрицы корреляций были определены силы связи целевого признака покупательская_активность со входными, используя шкалу Чеддока:

  • Высокая связь целевого признака прослеживается с входным признаком страниц_за_визит (0.75);
  • Средняя связь целевого признака прослеживается с входными признаками:
    • маркет_актив_6_мес (0.54),
    • акционные_покупки (0.51),
    • средний_просмотр_категорий_за_визит (0.54),
    • неоплаченные_продукты_штук_квартал (0.51),
    • страниц_за_визит (0.75),
    • выручка_препредыдущий_месяц (0.5),
    • минут_предыдущий_месяц (0.69),
    • минут_текущий_месяц (0.58);
  • Слабая связь целевого признака прослеживается с входным признаком популярная_категория (0.3);
  • Входные признаки: маркет_актив_тек_мес и ошибка_сервиса, имеют нулевую корреляцию с целевым признаком покупательская_активность. Это может указывать на то, что эти признаки не влияют на покупательскую активность.
  • С остальными входными признаками связь очень слабая.

Использование пайплайнов¶

In [48]:
df_ml = market_full.copy()
df_ml.sample(5)
Out[48]:
id покупательская_активность тип_сервиса разрешить_сообщать маркет_актив_6_мес маркет_актив_тек_мес длительность акционные_покупки популярная_категория средний_просмотр_категорий_за_визит неоплаченные_продукты_штук_квартал ошибка_сервиса страниц_за_визит выручка_препредыдущий_месяц выручка_предыдущий_месяц выручка_текущий_месяц минут_предыдущий_месяц минут_текущий_месяц
96 215448 Снизилась премиум да 4.6 4 974 0.94 Домашний текстиль 4 3 3 7 4670.0 5351.0 6129.7 9 10
899 216251 Прежний уровень стандарт да 4.0 4 320 0.24 Мелкая бытовая техника и электроника 5 4 3 8 4645.0 4565.5 4626.6 10 9
8 215358 Снизилась стандарт да 4.7 4 450 0.13 Домашний текстиль 4 2 6 4 4727.0 3488.0 4209.5 14 10
704 216056 Прежний уровень премиум да 4.6 4 871 0.39 Мелкая бытовая техника и электроника 5 2 2 10 5442.0 4636.5 4068.4 10 16
1175 216527 Прежний уровень стандарт нет 4.9 3 679 0.15 Техника для красоты и здоровья 4 1 1 14 5023.0 4668.0 5106.1 12 19

Выделим целевой признак покупательская_активность

In [49]:
X = df_ml.drop(['покупательская_активность', 'id'], axis=1)
y = df_ml['покупательская_активность']
In [50]:
# Разделяем данные на обучающую и тестовую выборки
X_train, X_test, y_train, y_test = train_test_split(
    X, # Входные признаки
    y, # Целевые признаки
    test_size=TEST_SIZE, # Размер тестовой выборки
    random_state=RANDOM_STATE, # Случайное состояние для воспроизводимости
    stratify=y
)
In [51]:
# Кодировка целевого признака
le = LabelEncoder()
y_train = le.fit_transform(y_train)
y_test = le.transform(y_test)
In [52]:
# Определение числовых и текстовых признаков
num_columns = X_train.select_dtypes(include=['int64', 'float64']).columns.tolist()
ohe_columns = X_train.select_dtypes(include=['object']).columns.tolist()
ohe_columns.remove('тип_сервиса')
ord_columns = ['тип_сервиса']
In [53]:
# создаём пайплайн для подготовки признаков из списка ohe_columns: заполнение пропусков и OHE-кодирование
# SimpleImputer + OHE
ohe_pipe = Pipeline(
    [
        (
            'simpleImputer_ohe', 
            SimpleImputer(missing_values=np.nan, strategy='most_frequent')
        ),
        (
            'ohe', 
            OneHotEncoder(drop='first', handle_unknown='ignore', sparse_output=False)
        )
    ]
)


ord_pipe = Pipeline(
    [
        (
            'simpleImputer_before_ord', 
            SimpleImputer(missing_values=np.nan, strategy='most_frequent')
        ),
        (
            'ord',  
            OrdinalEncoder(
                categories=[
                    ['стандарт', 'премиум'], 
                    ], 
                handle_unknown='use_encoded_value', unknown_value=np.nan
            )
        ),
        (
            'simpleImputer_after_ord', 
            SimpleImputer(missing_values=np.nan, strategy='most_frequent')
        )
    ]
) 


# создаём общий пайплайн для подготовки данных
data_preprocessor = ColumnTransformer(
    [
        ('ohe', ohe_pipe, ohe_columns),
        ('ord', ord_pipe, ord_columns),
        ('num', MinMaxScaler(), num_columns)
    ], 
    remainder='passthrough'
)


pipe_final = Pipeline(
    [
        ('preprocessor', data_preprocessor),
        ('model', DecisionTreeClassifier(random_state=RANDOM_STATE))
    ]
)


param_grid = [
    # словарь для модели KNeighborsClassifier() 
    {
        # название модели
        'model': [KNeighborsClassifier()],
        # указываем гиперпараметр модели n_neighbors
        'model__n_neighbors': range(1, 10),
        # указываем список методов масштабирования
        'preprocessor__num': [StandardScaler(), MinMaxScaler()]   
    },
    # словарь для модели DecisionTreeClassifier()
    {
        'model': [DecisionTreeClassifier(random_state=RANDOM_STATE)],
        'model__max_depth': range(1, 10),
        'model__max_features': range(1, 10),
        'preprocessor__num': [StandardScaler(), MinMaxScaler(), 'passthrough'],
    },
    # словарь для модели SVC()
    {
        'model': [SVC(probability=True, random_state=RANDOM_STATE)],
        'model__C': [0.1, 1, 10],
        'model__gamma': ['scale', 'auto', 0.1, 1],
        'preprocessor__num': [StandardScaler(), MinMaxScaler(), 'passthrough']  

    },
    # словарь для модели LogisticRegression()
    {
        'model': [LogisticRegression(solver='liblinear', penalty='l1', random_state=RANDOM_STATE)],
        'model__C': [0.1, 1, 10],
        'preprocessor__num': [StandardScaler(), MinMaxScaler(), 'passthrough']  
    }
]

Т.к. строк в датафрейме не много 1296, можно использовать метод GridSearchCV, он даст наиболее точный подбор гиперпараметров, т.к. пройдет по всем.

In [54]:
'''
models — инициализированная модель
param_grid — словарь с гиперпараметрами модели
cv — тип кросс-валидации
scoring — метрика, которую используем для выбора лучшего решения
n_jobs=-1 — подключаем к расчёту ядра процессора
'''
grid = GridSearchCV(
    pipe_final, 
    param_grid=param_grid,
    cv=5,
    scoring='roc_auc',
    n_jobs=-1
)

grid.fit(X_train, y_train)
print('Лучшая модель и её параметры:\n\n', grid.best_estimator_)
print ('Метрика лучшей модели на тренировочной выборке:', round(grid.best_score_, 4))
Лучшая модель и её параметры:

 Pipeline(steps=[('preprocessor',
                 ColumnTransformer(remainder='passthrough',
                                   transformers=[('ohe',
                                                  Pipeline(steps=[('simpleImputer_ohe',
                                                                   SimpleImputer(strategy='most_frequent')),
                                                                  ('ohe',
                                                                   OneHotEncoder(drop='first',
                                                                                 handle_unknown='ignore',
                                                                                 sparse_output=False))]),
                                                  ['разрешить_сообщать',
                                                   'популярная_категория']),
                                                 ('ord',
                                                  Pipeline(steps=[('simpleImputer_before_or...
                                                   'маркет_актив_тек_мес',
                                                   'длительность',
                                                   'акционные_покупки',
                                                   'средний_просмотр_категорий_за_визит',
                                                   'неоплаченные_продукты_штук_квартал',
                                                   'ошибка_сервиса',
                                                   'страниц_за_визит',
                                                   'выручка_препредыдущий_месяц',
                                                   'выручка_предыдущий_месяц',
                                                   'выручка_текущий_месяц',
                                                   'минут_предыдущий_месяц',
                                                   'минут_текущий_месяц'])])),
                ('model',
                 SVC(C=0.1, gamma=0.1, probability=True, random_state=42))])
Метрика лучшей модели на тренировочной выборке: 0.9121

Для данной задачи классификации была выбрана метрика качества ROC-AUC обладающая рядом преимуществ:

  • В отличие от точности (accuracy), которая зависит от фиксированного порога, ROC-AUC анализирует все возможные значения порога;
  • ROC-AUC является устойчивой метрикой при наличии дисбаланса классов (например, метрики точности или F1-меры могут быть смещены в сторону большинства).

Лучшей моделью является SVC (C=0.1, gamma=0.1, probability=True, random_state=42) и ядром rbf (он используется по стандарту если не указывать другие в пайплайне), количественные данные которой были закодированы StandardScaler(), а категориальные OneHotEncoder() и OrdinalEncoder().
Метрика ROC-AUC лучшей модели на тренировочной выборке (0.9121): Это показывает, что модель хорошо обучилась на тренировочных данных и смогла уловить большую часть закономерностей в данных.

In [55]:
# Предсказание на тестовой выборке
y_pred = grid.predict(X_test)

# Проверка на наличие метода predict_proba
if hasattr(grid.best_estimator_['model'], 'predict_proba'):
    # Предсказание вероятностей классов
    proba = grid.predict_proba(X_test)[:, 1]

    # Вычисление ROC-AUC на вероятностях
    roc_auc = roc_auc_score(y_test, proba)
    print(f'Метрика ROC-AUC на тестовой выборке: {roc_auc:.4f}')
else:
    print("Метод predict_proba не поддерживается для лучшей модели.")
Метрика ROC-AUC на тестовой выборке: 0.9112

Преобразуем признак акционные_покупки в категориальный (0 - акционные_покупки <= 0.65, 1 - акционные_покупки > 0.65). И посмотрим как изменится метрика качества.

In [56]:
df_ml_2 = df_ml.copy()
df_ml_2['акционные_покупки'] = df_ml_2['акционные_покупки'].apply(lambda x: 'более_0.65' if x > 0.65 else 'менее_0.65')
df_ml_2.drop('id', axis=1, inplace=True)
df_ml_2.head()
Out[56]:
покупательская_активность тип_сервиса разрешить_сообщать маркет_актив_6_мес маркет_актив_тек_мес длительность акционные_покупки популярная_категория средний_просмотр_категорий_за_визит неоплаченные_продукты_штук_квартал ошибка_сервиса страниц_за_визит выручка_препредыдущий_месяц выручка_предыдущий_месяц выручка_текущий_месяц минут_предыдущий_месяц минут_текущий_месяц
0 Снизилась премиум да 4.4 4 819 более_0.65 Товары для детей 4 4 2 5 4472.0 5216.0 4971.6 12 10
1 Снизилась стандарт нет 4.9 3 539 менее_0.65 Домашний текстиль 5 2 1 5 4826.0 5457.5 5058.4 8 13
2 Снизилась стандарт да 3.2 5 896 более_0.65 Товары для детей 5 0 6 4 4793.0 6158.0 6610.4 11 13
3 Снизилась стандарт нет 5.1 3 1064 более_0.65 Товары для детей 3 2 3 2 4594.0 5807.5 5872.5 8 11
4 Снизилась стандарт да 3.3 4 762 менее_0.65 Домашний текстиль 4 1 1 4 5124.0 4738.5 5388.5 10 10
In [57]:
col_names_corr = df_ml_2.select_dtypes(include='number').columns.to_list()
col_names_corr
Out[57]:
['маркет_актив_6_мес',
 'маркет_актив_тек_мес',
 'длительность',
 'средний_просмотр_категорий_за_визит',
 'неоплаченные_продукты_штук_квартал',
 'ошибка_сервиса',
 'страниц_за_визит',
 'выручка_препредыдущий_месяц',
 'выручка_предыдущий_месяц',
 'выручка_текущий_месяц',
 'минут_предыдущий_месяц',
 'минут_текущий_месяц']
In [58]:
big_data_corr = df_ml_2.phik_matrix(interval_cols=col_names_corr)

plt.figure(figsize=(14, 10))
sns.heatmap(big_data_corr, annot=True, fmt=".2f", center=0)
plt.title('Матрица корреляций phik для модели df_ml_2')
plt.show()
No description has been provided for this image

Мультиколлинеарности после перевода входного признака акционные_покупки в категориальный - не обнаружено

In [59]:
X_new = df_ml_2.drop(['покупательская_активность'], axis=1)
y_new = df_ml_2['покупательская_активность']
In [60]:
# Разделяем данные на обучающую и тестовую выборки
X_train_new, X_test_new, y_train_new, y_test_new = train_test_split(
    X_new, # Входные признаки
    y_new, # Целевые признаки
    test_size=TEST_SIZE, # Размер тестовой выборки
    random_state=RANDOM_STATE, # Случайное состояние для воспроизводимости
    stratify=y_new
)

# Кодировка целевого признака
y_train_new = le.transform(y_train_new)
y_test_new = le.transform(y_test_new)

# Определение числовых и текстовых признаков
num_columns = X_train_new.select_dtypes(include=['int64', 'float64']).columns.tolist()
ohe_columns = X_train_new.select_dtypes(include=['object']).columns.tolist()
ohe_columns.remove('тип_сервиса')
ord_columns = ['тип_сервиса']
In [61]:
# создаём общий пайплайн для подготовки данных
data_preprocessor_2 = ColumnTransformer(
    [
        ('ohe', ohe_pipe, ohe_columns), 
        ('ord', ord_pipe, ord_columns),
        ('num', MinMaxScaler(), num_columns)
    ], 
    remainder='passthrough'
)
In [62]:
pipe_final_2 = Pipeline(
    [
        ('preprocessor', data_preprocessor_2),
        ('model', DecisionTreeClassifier(random_state=RANDOM_STATE))
    ]
)
In [63]:
'''
models — инициализированная модель
param_grid — словарь с гиперпараметрами модели
cv — тип кросс-валидации
scoring — метрика, которую используем для выбора лучшего решения
n_jobs=-1 — подключаем к расчёту ядра процессора
'''
grid_2 = GridSearchCV(
    pipe_final_2, 
    param_grid=param_grid,
    cv=5,
    scoring='roc_auc',
    n_jobs=-1
)

grid_2.fit(X_train_new, y_train_new)
print('Лучшая модель и её параметры:\n\n', grid_2.best_estimator_)
print ('Метрика лучшей модели на тренировочной выборке:', round(grid_2.best_score_, 4))
Лучшая модель и её параметры:

 Pipeline(steps=[('preprocessor',
                 ColumnTransformer(remainder='passthrough',
                                   transformers=[('ohe',
                                                  Pipeline(steps=[('simpleImputer_ohe',
                                                                   SimpleImputer(strategy='most_frequent')),
                                                                  ('ohe',
                                                                   OneHotEncoder(drop='first',
                                                                                 handle_unknown='ignore',
                                                                                 sparse_output=False))]),
                                                  ['разрешить_сообщать',
                                                   'акционные_покупки',
                                                   'популярная_категория']),
                                                 ('ord',
                                                  Pipeline(steps=[('sim...
                                                  ['маркет_актив_6_мес',
                                                   'маркет_актив_тек_мес',
                                                   'длительность',
                                                   'средний_просмотр_категорий_за_визит',
                                                   'неоплаченные_продукты_штук_квартал',
                                                   'ошибка_сервиса',
                                                   'страниц_за_визит',
                                                   'выручка_препредыдущий_месяц',
                                                   'выручка_предыдущий_месяц',
                                                   'выручка_текущий_месяц',
                                                   'минут_предыдущий_месяц',
                                                   'минут_текущий_месяц'])])),
                ('model',
                 SVC(C=1, gamma=0.1, probability=True, random_state=42))])
Метрика лучшей модели на тренировочной выборке: 0.9081

Лучшей моделью также является SVC (C=0.1, gamma=0.1, probability=True, random_state=42) и ядром rbf, количественные данные которой были закодированы StandardScaler(), а категориальные OneHotEncoder() и OrdinalEncoder().
Метрика ROC-AUC лучшей модели на тренировочной выборке (0.9081): это чуть меньше, чем у предыдущей.

In [64]:
# Предсказание на тестовой выборке
y_pred_new = grid_2.predict(X_test_new)

# Проверка на наличие метода predict_proba
if hasattr(grid_2.best_estimator_['model'], 'predict_proba'):
    # Предсказание вероятностей классов
    proba_new = grid_2.predict_proba(X_test_new)[:, 1]

    # Вычисление ROC-AUC на вероятностях
    roc_auc_new = roc_auc_score(y_test_new, proba_new)
    print(f'Метрика ROC-AUC на тестовой выборке: {roc_auc_new:.4f}')
else:
    print("Метод predict_proba не поддерживается для лучшей модели.")
Метрика ROC-AUC на тестовой выборке: 0.9129

Сравнение метрики качества моделей на тренировочной и тестовой выборке при категориальном и количественном признаке акционные_покупки

In [65]:
data = {
    'акционные_покупки_num': [round(grid.best_score_, 4), round(roc_auc, 4)],
    'акционные_покупки_cat': [round(grid_2.best_score_, 4), round(roc_auc_new, 4)],
}

table = pd.DataFrame(data, index=['train', 'test'])
table
Out[65]:
акционные_покупки_num акционные_покупки_cat
train 0.9121 0.9081
test 0.9112 0.9129

На тренировочных данных лучшую метрику ROC-AUC (0.9112) показала SVC модель с количественным входным признаком акционные_покупки, а на тестовых SVC модель с категориальным входным признаком акционные_покупки.

Принимая факт, что наивысшая и наиболее важная метрика, та которая показана на тренировочных данных. Поэтому для дальнейшего иследования выберем первую модель с SVC модель с количественным входным признаком акционные_покупки.


Обший вывод по работе с пайплайнами

В результате выполнения моделирования, была выбрана лучшая модель и её параметры. Лучшей моделью является - SVC (C=0.1, gamma=0.1, probability=True, random_state=42) и ядром rbf, количественные данные которой были закодированы StandardScaler(), а категориальные OneHotEncoder() и OrdinalEncoder().

Метрика ROC-AUC лучшей модели на тренировочной выборке (0.9121): Это показывает, что модель хорошо обучилась на тренировочных данных и смогла уловить большую часть закономерностей в данных.

Метрика ROC-AUC на тестовой выборке (0.9112).

Таким образом, выбранная модель показала хорошие результаты и может быть использована для прогнозирования покупательской активности на основе предоставленных данных.


Анализ важности признаков¶

In [66]:
# Преобразуем тренировочные данные
X_train_enc = grid.best_estimator_['preprocessor'].fit_transform(X_train)

# Получаем модель
model = grid.best_estimator_['model']

# Используем shap.sample для выборки подмножества данных
sample_data = shap.sample(X_train_enc, 100)  # выбираем 100 случайных примеров

# Создаем объяснитель SHAP с PermutationExplainer
explainer = shap.PermutationExplainer(model.predict_proba, sample_data)

# Преобразуем тестовые данные
X_test_enc = grid.best_estimator_['preprocessor'].transform(X_test)

# Получаем имена признаков
feature_names = grid.best_estimator_['preprocessor'].get_feature_names_out()

# Создаем DataFrame для удобства анализа
X_test_enc = pd.DataFrame(X_test_enc, columns=feature_names)

# Вычисляем SHAP-значения
shap_values = explainer.shap_values(X_test_enc)
PermutationExplainer explainer: 325it [02:47,  1.85it/s]                                                               
In [67]:
shap_values_class_1 = shap.Explanation(
    values=shap_values[:, :, 1],          # SHAP-значения для второго класса
    feature_names=X_test_enc.columns,     # Имена признаков
    data=X_test_enc.values                # Исходные данные
)

# Визуализируем важность признаков для второго класса
shap.plots.bar(shap_values_class_1, max_display=30)
No description has been provided for this image
In [68]:
shap.plots.beeswarm(shap_values_class_1, max_display=30)
No description has been provided for this image

На диаграмме beeswarm показана важность разных признаков для модели, которая предсказывает покупательскую активность. Признаки с положительными значениями SHAP повышают вероятность того, что покупательская активность снизится (класс 1), а признаки с отрицательными значениями - понижают (класс 0).


Вывод: Входные признаки, num__страниц_за_визит, num__минут_предыдущий_месяц, num__минут_текущий_месяц и num__акционные_покупки, являются наиболее значимыми, так как они имеют наибольшее влияние на покупательскую_активность.

Входные признаки, ohe__популярная_категория..., ohe__тип_сервиса_стандарт и ohe__разрешить_сообщать_нет не имеют влияние и могут быть менее важными для модели.

Данные по значимости признаков помогут определить бизнесу какие факторы влияют на сохранение активности покупателя на сайте. Так из данных выше можно сделать вывод, что время проведенное на сайте сильнее всего влияет на активность клиента, а следовательно и на прибыль магазина. Это можно объяснить тем, что у пользователя есть большой выбор товаров для покупки. И ему не нужно искать другие способы покупки необходимого ему товара (например на другом сайте).


Сегментация покупателей¶

In [69]:
# Преобразуем predictions_train и predictions_test в pandas Series
predictions_train = pd.Series(grid.predict_proba(X_train)[:, 1], index=X_train.index)
predictions_test = pd.Series(proba_new, index=X_test.index)

# Объединяем вероятности вдоль строк (по индексу)
predictions_final = pd.concat([predictions_train, predictions_test])

# Создаём копию исходного DataFrame, который учавствовал в обучении модели
df_full = df_ml.copy()

# Проверяем совпадение длины перед добавлением нового столбца
if len(predictions_final) == len(df_full):
    df_full['вероятность_снижения_активности'] = predictions_final
else:
    raise ValueError("Длина predictions_final не совпадает с количеством строк в df_full.")

# Отображаем примеры итогового DataFrame
df_full.sample(5)
Out[69]:
id покупательская_активность тип_сервиса разрешить_сообщать маркет_актив_6_мес маркет_актив_тек_мес длительность акционные_покупки популярная_категория средний_просмотр_категорий_за_визит неоплаченные_продукты_штук_квартал ошибка_сервиса страниц_за_визит выручка_препредыдущий_месяц выручка_предыдущий_месяц выручка_текущий_месяц минут_предыдущий_месяц минут_текущий_месяц вероятность_снижения_активности
145 215497 Снизилась премиум да 5.6 5 719 0.28 Мелкая бытовая техника и электроника 1 2 7 1 4475.0 5871.5 6277.1 9 5 0.959532
151 215503 Снизилась стандарт да 2.4 5 638 0.24 Домашний текстиль 1 5 2 1 4119.0 5046.0 4886.1 7 8 0.988212
343 215695 Снизилась стандарт да 4.3 4 324 0.14 Техника для красоты и здоровья 2 7 4 4 5160.0 5314.0 5310.1 18 20 0.492313
201 215553 Снизилась стандарт да 0.9 4 360 0.33 Домашний текстиль 2 3 3 5 4309.0 4138.0 4594.3 6 10 0.968069
1177 216529 Прежний уровень стандарт нет 3.5 3 757 0.25 Товары для детей 3 1 2 7 4974.0 4340.0 5019.9 11 17 0.103883
In [70]:
# Выполняем объединение с использованием merge
df_full = df_full.merge(money, on='id', how='inner')
df_full.sample(5)
Out[70]:
id покупательская_активность тип_сервиса разрешить_сообщать маркет_актив_6_мес маркет_актив_тек_мес длительность акционные_покупки популярная_категория средний_просмотр_категорий_за_визит неоплаченные_продукты_штук_квартал ошибка_сервиса страниц_за_визит выручка_препредыдущий_месяц выручка_предыдущий_месяц выручка_текущий_месяц минут_предыдущий_месяц минут_текущий_месяц вероятность_снижения_активности прибыль
1219 216571 Прежний уровень стандарт нет 4.3 4 445 0.30 Техника для красоты и здоровья 2 2 4 19 4588.0 4772.5 5062.4 14 20 0.392687 2.27
1089 216441 Прежний уровень стандарт нет 4.1 4 136 0.30 Мелкая бытовая техника и электроника 3 4 3 10 4485.0 4356.0 4374.2 20 14 0.091297 1.87
722 216074 Прежний уровень премиум да 3.7 3 828 0.14 Товары для детей 3 3 3 7 5362.0 5094.0 5456.7 18 12 0.091238 5.90
948 216300 Прежний уровень стандарт да 3.7 4 714 0.26 Техника для красоты и здоровья 6 3 3 9 5039.0 5121.0 5179.5 14 16 0.034554 3.98
211 215563 Снизилась премиум да 3.9 5 956 0.35 Косметика и аксесуары 2 2 6 7 4985.0 6036.5 6114.5 10 9 0.840592 6.88

Преобразуем столбец акционные_покупки в категориальный признак для упрощения исследования категории клиентов часто покупающих по скидкам

In [71]:
df_full['акционные_покупки'] = df_full['акционные_покупки'].apply(lambda x: 'более_0.65' if x > 0.65 else 'менее_0.65')
df_full.sample(5)
Out[71]:
id покупательская_активность тип_сервиса разрешить_сообщать маркет_актив_6_мес маркет_актив_тек_мес длительность акционные_покупки популярная_категория средний_просмотр_категорий_за_визит неоплаченные_продукты_штук_квартал ошибка_сервиса страниц_за_визит выручка_препредыдущий_месяц выручка_предыдущий_месяц выручка_текущий_месяц минут_предыдущий_месяц минут_текущий_месяц вероятность_снижения_активности прибыль
1142 216494 Прежний уровень премиум да 5.1 3 1041 менее_0.65 Кухонная посуда 2 0 2 4 5428.0 4820.5 4424.0 11 18 0.173000 5.10
1238 216590 Прежний уровень премиум да 6.6 5 740 менее_0.65 Мелкая бытовая техника и электроника 4 2 6 14 5446.0 5566.0 5247.5 19 14 0.091249 3.63
1027 216379 Прежний уровень премиум да 3.3 4 827 менее_0.65 Домашний текстиль 4 2 4 14 4606.0 5105.5 5668.4 13 13 0.077012 3.68
1158 216510 Прежний уровень премиум да 4.9 5 723 более_0.65 Мелкая бытовая техника и электроника 2 4 6 9 5290.0 5821.0 6411.2 20 12 0.191286 3.76
754 216106 Прежний уровень стандарт да 4.4 5 360 менее_0.65 Товары для детей 4 4 3 11 4454.0 3865.0 4739.9 14 12 0.113733 3.46

Попытаемся выделить сегмент покупателей с высокой вероятностью снижения активности и исследовать его

In [72]:
df_full['спад_активности'] = df_full['вероятность_снижения_активности'].apply(lambda x: 'уходящий_клиент' if x >= 0.75 else 'обычный_клиент')
df_low_act = df_full.query('спад_активности == "уходящий_клиент"')
display(df_low_act.shape)
df_low_act.sample(5)
(365, 21)
Out[72]:
id покупательская_активность тип_сервиса разрешить_сообщать маркет_актив_6_мес маркет_актив_тек_мес длительность акционные_покупки популярная_категория средний_просмотр_категорий_за_визит ... ошибка_сервиса страниц_за_визит выручка_препредыдущий_месяц выручка_предыдущий_месяц выручка_текущий_месяц минут_предыдущий_месяц минут_текущий_месяц вероятность_снижения_активности прибыль спад_активности
111 215463 Снизилась стандарт нет 3.1 4 472 более_0.65 Домашний текстиль 2 ... 3 3 4470.0 5196.0 5015.3 8 12 0.995942 3.08 уходящий_клиент
116 215468 Снизилась стандарт да 2.7 4 515 более_0.65 Косметика и аксесуары 1 ... 2 2 4698.0 6060.0 6346.0 9 11 0.988464 4.62 уходящий_клиент
137 215489 Снизилась стандарт нет 3.6 5 344 более_0.65 Товары для детей 3 ... 4 4 4817.0 6183.5 6594.3 9 8 0.969915 3.68 уходящий_клиент
75 215427 Снизилась стандарт нет 2.4 4 186 менее_0.65 Косметика и аксесуары 3 ... 7 6 4795.0 5549.5 6427.0 9 14 0.902136 2.76 уходящий_клиент
311 215663 Снизилась стандарт да 2.6 4 302 менее_0.65 Товары для детей 1 ... 5 4 4291.0 5303.5 5482.9 5 6 0.989677 3.69 уходящий_клиент

5 rows × 21 columns

Проведем исследовательский анализ группы уходящий_клиент (аналогично тому что проводили в 3 пункте)

In [73]:
# Лист с названиями столбцов категориальных признаков
dict_df_full_cat = df_full.select_dtypes(include=['object']).columns.to_list()

dict_df_full_cat = [col for col in dict_df_full_cat if col not in ['спад_активности', 'покупательская_активность']]

for col in dict_df_full_cat:
    show_cat_variable_by_target(df_full, col, col, 'спад_активности')
    print('=' * TERM_SIZE)
No description has been provided for this image
====================================================================================================================================================================================
No description has been provided for this image
====================================================================================================================================================================================
No description has been provided for this image
====================================================================================================================================================================================
No description has been provided for this image
====================================================================================================================================================================================

Клиенты с высокой вероятностью снижения активности, чаще остальных покупают товары по скидке.
Попробуем провести анализ данной группы клиентов. Предварительно поместив таких покупателей в отдельный сегмент уходящий_скидочник.

In [74]:
df_full['уходящий_скидочник'] = df_full.apply(
    lambda row: 'да' if row['спад_активности'] == "уходящий_клиент" and row['акционные_покупки'] == "более_0.65" else 'нет',
    axis=1
)
df_full.query('уходящий_скидочник == "да"')
Out[74]:
id покупательская_активность тип_сервиса разрешить_сообщать маркет_актив_6_мес маркет_актив_тек_мес длительность акционные_покупки популярная_категория средний_просмотр_категорий_за_визит ... страниц_за_визит выручка_препредыдущий_месяц выручка_предыдущий_месяц выручка_текущий_месяц минут_предыдущий_месяц минут_текущий_месяц вероятность_снижения_активности прибыль спад_активности уходящий_скидочник
0 215349 Снизилась премиум да 4.4 4 819 более_0.65 Товары для детей 4 ... 5 4472.0 5216.0 4971.6 12 10 0.964315 4.16 уходящий_клиент да
3 215352 Снизилась стандарт нет 5.1 3 1064 более_0.65 Товары для детей 3 ... 2 4594.0 5807.5 5872.5 8 11 0.947265 4.21 уходящий_клиент да
13 215364 Снизилась премиум да 4.3 4 708 более_0.65 Домашний текстиль 3 ... 3 4942.0 5795.5 5484.8 11 9 0.994606 2.67 уходящий_клиент да
14 215365 Снизилась стандарт да 3.9 4 167 более_0.65 Техника для красоты и здоровья 6 ... 5 4190.0 4577.0 4799.3 6 10 0.810166 3.65 уходящий_клиент да
22 215373 Снизилась премиум нет 3.8 3 811 более_0.65 Товары для детей 2 ... 3 4293.0 4632.0 5161.1 10 8 0.993379 3.69 уходящий_клиент да
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
393 215745 Снизилась стандарт да 1.7 4 550 более_0.65 Мелкая бытовая техника и электроника 4 ... 6 4990.0 5654.5 6126.4 9 9 0.897747 5.10 уходящий_клиент да
395 215747 Снизилась стандарт да 3.5 5 452 более_0.65 Товары для детей 1 ... 4 4168.0 3555.0 4089.3 10 10 0.975485 3.32 уходящий_клиент да
490 215842 Снизилась премиум да 4.3 3 1036 более_0.65 Товары для детей 4 ... 9 4642.0 4408.0 4740.8 8 10 0.941479 1.33 уходящий_клиент да
558 215910 Снизилась стандарт нет 3.9 3 509 более_0.65 Косметика и аксесуары 2 ... 5 4874.0 5360.0 5911.2 11 17 0.948036 4.61 уходящий_клиент да
1252 216604 Прежний уровень стандарт да 4.9 5 350 более_0.65 Домашний текстиль 3 ... 5 4735.0 4545.0 4840.1 14 14 0.750895 2.88 уходящий_клиент да

114 rows × 22 columns

Получили 114 клиентов удволетворяющих критерию уходящий_скидочник.

In [75]:
# Формирование списка столбцов с количественными признаками
num_variables_col = df_full.select_dtypes(include=['number']).columns.to_list()
num_variables_col
Out[75]:
['id',
 'маркет_актив_6_мес',
 'маркет_актив_тек_мес',
 'длительность',
 'средний_просмотр_категорий_за_визит',
 'неоплаченные_продукты_штук_квартал',
 'ошибка_сервиса',
 'страниц_за_визит',
 'выручка_препредыдущий_месяц',
 'выручка_предыдущий_месяц',
 'выручка_текущий_месяц',
 'минут_предыдущий_месяц',
 'минут_текущий_месяц',
 'вероятность_снижения_активности',
 'прибыль']
In [76]:
num_variables_col = [col for col in num_variables_col if col not in ['id', 'вероятность_снижения_активности']]
In [77]:
# Вывод графиков для датафрейма market_file
for col in num_variables_col:
    show_num_variable(df_full, col, 'уходящий_скидочник')
    print('=' * TERM_SIZE)
No description has been provided for this image
====================================================================================================================================================================================
No description has been provided for this image
====================================================================================================================================================================================
No description has been provided for this image
====================================================================================================================================================================================
No description has been provided for this image
====================================================================================================================================================================================
No description has been provided for this image
====================================================================================================================================================================================
No description has been provided for this image
====================================================================================================================================================================================
No description has been provided for this image
====================================================================================================================================================================================
No description has been provided for this image
====================================================================================================================================================================================
No description has been provided for this image
====================================================================================================================================================================================
No description has been provided for this image
====================================================================================================================================================================================
No description has been provided for this image
====================================================================================================================================================================================
No description has been provided for this image
====================================================================================================================================================================================
No description has been provided for this image
====================================================================================================================================================================================

Скидочники проводят меньше времени на сайте, чем другие покупатели. Вероятно они отслеживают цену определенного необходимого им товара;
Так же они чаще неоплачивают товары в корзине. Скорее всего они помещают нужные им товары в корзину (для более быстрого поиска в дальнейшем) и ждут их скидки;
Скидочники в среднем приносят магазину такую же прибыль как и другие покупатели, поэтому эта категория важна магазину и с ней необходимо считаться.

In [78]:
# Лист с названиями столбцов категориальных признаков
dict_df_full_cat = df_full.select_dtypes(include=['object']).columns.to_list()
dict_df_full_cat
Out[78]:
['покупательская_активность',
 'тип_сервиса',
 'разрешить_сообщать',
 'акционные_покупки',
 'популярная_категория',
 'спад_активности',
 'уходящий_скидочник']
In [79]:
dict_df_full_cat = [col for col in dict_df_full_cat if col not in ['уходящий_скидочник', 'покупательская_активность', 'акционные_покупки', 'спад_активности']]

for col in dict_df_full_cat:
    show_cat_variable_by_target(df_full, col, col, 'уходящий_скидочник')
    print('=' * TERM_SIZE)
No description has been provided for this image
====================================================================================================================================================================================
No description has been provided for this image
====================================================================================================================================================================================
No description has been provided for this image
====================================================================================================================================================================================
In [80]:
stats_df = df_full.groupby('уходящий_скидочник')[num_variables_col].describe().round(3).T
try:
    stats_df.to_excel('output_2.xlsx')
except:
    display(stats_df)

Общий вывод:

На основе данных смоделированной модели был выделен сегмент покупателей под названием уходящий_скидочник (Клиенты с высокой вероятностью снижения активности, часто покупающие товары по скидке);

Уходящий_скидочник чаще пользуются типом сервиса премиум, возможно это связано с увеличенной скидкой или большим числом товаров находящихся по акции;
Так же уходящий_скидочник чаще других покупает товары для детей.

На основе этих наблюдений были сделаны следующие предложения для работы с сегментом уходящий_скидочник:

  1. Увеличение количества акций и скидок.
  2. Улучшение системы рекомендаций.
  3. Работа с неоплаченными продуктами через специальные скидки или напоминания.
  4. Создание персонализированных предложений на основе данных о популярных категориях покупателя.
  5. Использование рассылки для отправки персонализированных предложений и информировании о проходящих акциях.
  6. Проведение более частых акций для категории товаров - товары для детей.
  7. Т.к. данный сегмент покупателей чаще используют премиум сервис, можно специально для них добавить новую подписку с увеличенной скидкой на определенные товары.

Общий вывод¶


  • В данной работе была рассмотрена задача по разработке решения для интернет-магазина «В один клик», которое поможет увеличить покупательскую активность постоянных клиентов через персонализированные предложения. Для была предсказана вероятность снижения покупательской активности клиента в следующие три месяца, выделен сегмент покупателей уходящий_скидочник и разработаны для них персонализированные предложения.
  • Был проведен предварительный анализ данных в ходе которого получили:
    • В датафреймах отсвутствуют пропуски;
    • Название столбцов датафреймов необходимо привести к snake_case;
    • Нет ошибок в типах данных в датафреймах;
    • Столбцы id в датафреймах сделали значениями индексов;
    • Была получена общая информация о датафреймах.
  • Была проведена предобработка данных:
    • Приведены названия столбцов датафреймов к snake_case;
    • Проверены датафреймы на дубликаты и выявлено их отсутствие.
  • Был выполнен исследовательский анализ данных (пункт 3);
  • Было проведено объедение данных датафреймов: market_file, market_money и market_time, для клиентов с наличием покупок в 3-х последних месяцах;
  • Был проведен корреляционный анализ данных (пункт 5);
  • Была выполнена задача классификации с использованием метода пайплайнов в МО. В результате было получено:
    • В результате выполнения моделирования, была выбрана лучшая модель и её параметры. Лучшей моделью стала - SVC (C=0.1, gamma=0.1, probability=True, random_state=42) и ядром rbf, количественные данные которой были закодированы методом StandardScaler(), а категориальные OneHotEncoder() и OrdinalEncoder(). Метрика ROC-AUC лучшей модели на тренировочной выборке (0.9121): Это показывает, что модель хорошо обучилась на тренировочных данных и смогла уловить большую часть закономерностей в данных. Метрика ROC-AUC на тестовой выборке (0.9112).
  • Был проведен анализ влияния входных признаков на целевой (покупательская_активность) используя метод SHAP:
    • Входные признаки, num__страниц_за_визит, num__минут_предыдущий_месяц и num__минут_текущий_месяц, являются наиболее значимыми, так как они имеют наибольшее влияние на покупательскую_активность.
      Входные признаки, ohe__популярная_категория... (кроме бытовой техники и электроники), ohe__тип_сервиса_стандарт и ohe__разрешить_сообщать_нет не имеют влияние и могут быть менее важными для модели.
      Данные по значимости признаков помогут определить бизнесу какие факторы влияют на сохранение активности покупателя на сайте. Так из данных выше можно сделать вывод, что время проведенное на сайте сильнее всего влияет на активность клиента, а следовательно и на прибыль магазина. Это можно объяснить тем, что у пользователя есть большой выбор товаров для покупки. И ему не нужно искать другие способы покупки необходимого ему товара (например на другом сайте).
  • Была выполнена сегментация клиентов интернет-магазина «В один клик». Они характеризуются меньшим временем на сайте, узкой фокусировкой на товарах со скидками и большим количеством неоплаченных товаров. Для стимулирования их активности предлагается увеличить количество акций, улучшить систему рекомендаций, работать с неоплаченными товарами, создать дополнительные скидочные подписки и использовать рассылку для отправки персонализированных предложений.